在页面线程中,虽然可以直接使用底层 API 来处理 Service Worker 的注册、更新与通信,但在较为复杂的应用场景下(比如,页面中不同窗口注册不同的 Service Worker),我们往往会因为要处理各种情况而逐步陷入复杂、混乱的深渊,并且,在出现运行结果与预期结果不一致时,我们往往不知所措、不知如何进行排查。
正是因为这些原因,Workbox 提供了运行在页面线程中的 workbox-window 模块,通过该模块,我们可以:
- 更便捷、高效地处理 Service Worker 地注册、更新及通信。
- 通过运行时完善的日志输出(比如 Service Worker 生命周期状态改变),可帮助我们快速定位运行错误;亦可通过日志的提示(比如注册 Service Worker 时,指定了错误的 scope),帮助我们避免犯一些常见错误。
- 接下来,我们将一起学习 workbox-window 模块的使用。
# 基本使用
要使用 workbox-window,我们需要通过 npm 来安装相关依赖:
$ npm install --save workbox-window
或使用 yarn:
$ yarn add workbox-window
然后在代码文件中引入相关模块:
import { Workbox } from 'workbox-window/Workbox.mjs';
为了在开发环境中 workbox-window 能够输出日志,我们必须从
workbox-window/Workbox.mjs中引入 Workbox 模块,并按照以下方式修改webpack.config.js文件:
const Terser = require('terser-webpack-plugin');
const { EnvironmentPlugin } = require('webpack');
module.exports = {
//... 其他配置
optimization: {
minimizer: [
new Terser({
test: /\.m?js$/
})
]
},
plugins: [
//... 其他插件
new EnvironmentPlugin({
NODE_ENV: 'development'
})
]
};
接下来,我们便可通过以下方式进行 Service Worker 的注册:
if ('serviceWorker' in navigator) {
const workbox = new Workbox('/sw.js', { scope: '/' });
workbox.register({ immediate: false });
}
示例中,我们首先声明了 Workbox 的实例对象 workbox,然后调用其实例方法 register 进行 Service Worker 的注册,其中:
Workbox 构造函数的参数与方法 navigator.serviceWorker.register 的参数一样,此处不再重述。
workbox.register方法的参数为含有immediate属性的对象,该属性表示是否立即注册 Service Worker,而无需等待页面元素加载完成,默认值为 false。当该属性的值为 false 时,我们无需显式监听页面的 load 事件,因此以下代码:
window.addEventListener('load', () => {
workbox.register();
});
可简化为:
workbox.register();
# 更新管理
我们可通过监听
ServiceWorker的 statechange 事件、ServiceWorkerRegistration的updatefound事件以及ServiceWorkerContainer的controllerchange事件来处理 Service Worker 的更新:
navigator.serviceWorker.register('/sw.js').then(registration => {
if (registration.waiting) {
//通知用户有更新,执行更新操作...
}
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
setTimeout(() => {
if (newWorker.state === 'installed') {
//通知用户有更新,执行更新操作...
}
}, 200);
}
});
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
//通知用户更新已完成,执行页面刷新操作...
});
上例中,为了能够准确无误地处理更新,除了要求我们对 Service Worker 生命周期有深刻清晰的认识,也需要我们自行处理可能出现的任何状况,此过程繁琐且易于出错;基于此,workbox-window 在内部封装了这些细节,并通过一些简单明了的事件来帮助开发者便捷、高效地处理 Service Worker 更新问题。
在介绍 workbox-window 的生命周期事件之前,我们先对已注册 Service Worker 及外部 Service Worker 进行简单说明:
Service Worker 注册成功后,如果后续触发了 updatefound 事件(workbox-window 内部会主动监听该事件),新安装的 Service Worker 只有满足以下任何一个条件后,才会被当作外部 Service Worker,否则为已注册 Service Worker:
updatefound事件被触发的次数大于一次。updatefound事件被触发与workbox.register被调用的时间差大于 1 分钟。- 新安装
Service Worker脚本地址与注册的地址不一致。
了解了已注册 Service Worker 及外部 Service Worker,下面我们来一起看下 workbox-window 所提供的生命周期事件:
installed:新的 Service Worker 已安装,且新安装的 Service Worker 为已注册 Service Worker 时触发。waiting:- 执行
workbox.register方法时,如果registration.waiting的值不为空,触发该事件,且事件参数 event 的wasWaitingBeforeRegister属性值为true。 - 新的 Service Worker 已安装,且 200 毫秒后(等待以确保 Service Worker 在 install 事件中没有调用
skipWaiting方法)新安装的 Service Worker 状态依旧为 installed,并且新安装的 Service Worker 为已注册 Service Worker 时触发。
- 执行
controlling:事件controllerchange被触发,且新激活的 Service Worker 为已注册Service Worker时触发。activated:Service Worker已激活,且已激活的 Service Worker 为已注册 Service Worker 时触发。externalinstalled:新的 Service Worker 已安装,且新安装的 Service Worker 为外部 Service Worker 时触发。externalwaiting:新的 Service Worker 已安装,且 200 毫秒后(等待以确保 Service Worker 在 install 事件中没有调用 skipWaiting 方法)新安装的 Service Worker 状态依旧为 installed,并且新安装的 Service Worker 为外部 Service Worker 时触发。externalactivated:新的 Service Worker 已激活,且新激活的 Service Worker 为外部 Service Worker 时触发。
了解了 workbox-window 的生命周期事件,我们便可以按照以下方式修改前文所述的 Service Worker 更新示例:
const workbox = new Workbox('/sw.js');
workbox.addEventListener('waiting', event => {
//通知用户有更新,执行更新操作...
});
workbox.addEventListener('externalwaiting', event => {
//通知用户有更新,执行更新操作...
});
workbox.addEventListener('activated', event => {
if (event.isUpdate) {
//通知用户更新已完成,执行页面刷新操作...
}
});
workbox.addEventListener('externalactivated', event => {
//通知用户更新已完成,执行页面刷新操作...
});
workbox.register();
示例中,我们需要注意以下两点:
由于在执行
workbox.register方法时,如果registration.waiting的值不为空,便会在当前调用栈为空时立即触发 waiting 事件,如要捕获此刻的waiting事件,应在workbox.register执行之前注册事件监听。
由于在首次注册 Service Worker 时亦会触发 activated 事件,因此需要通过 event.isUpdate 判断来避免首次注册时执行不必要的逻辑。
# 通信管理
我们可以调用
workbox.messageSW方法向 Service Worker 发送消息,并以Promise返回值的形式得到 Service Worker 的响应,比如:
//sw.js
const SW_VERSION = '1.0.0';
self.addEventListener('message', event => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
//index.html
const swVersion = await workbox.messageSW({ type: 'GET_VERSION' });
console.log('Service Worker version:', swVersion);
示例中:
- 首先,在 Service Worker 中监听 message 事件,并且当
event.data.type的值为GET_VERSION时,通过event.ports[0]发送响应给页面; - 然后,在页面中,我们通过调用
workbox.messageSW方法来发送类型为GET_VERSION的消息,并通过方法的返回值(类型为 Promise)获得 Service Worker 的响应。
messageSW方法的参数可以是任意类型,但还是建议使用含有以下属性的对象:
type:Service Worker 需要根据该属性的值执行不同的业务逻辑,因此该属性的值需要全局唯一(类型为字符串,单词全部大写,且单词之间用下划线分割)。meta:主要用于放置一些额外信息,且该信息不属于 payload 的一部分;在 Workbox 中,该属性的值为消息发送方所在模块的名称(比如:workbox-broadcast-cache-update),自定义消息时可不指定,或自行指定(类型为字符串)。payload:需要发送的实际数据(任意类型)。
由于
messageSW的实现基于MessageChannel(详情参见 Workbox 详解篇:缓存更新广播中的相关讨论),因此在 Service Worker 端必须使用event.ports[0].postMessage` 来给页面发送响应,如用其他方式或不发送响应,那么 messageSW 的返回值将永远不会 resolve。
除了向 Service Worker 发送消息外,我们还可以通过监听 workbox 的 message 事件来接收 Service Worker 主动发送的消息:
workbox.addEventListener('message', event => {
//doSomething...
});
由于 workbox 的 message 事件内部同时监听了通过 postMessage 和 BroadcastChannel(详情参见 Workbox 详解篇:缓存更新广播中的相关讨论)发送的消息,因此在 Service Worker 中如果使用 BroadcastChannel 发送消息,那么 BroadcastChannel 构造参数的值必须为 workbox。
# 总结
本章我们首先介绍了 workbox-window 模块的使用;然后介绍了如何通过所提供的生命周期事件来高效地处理 Service Worker 更新问题;最后我们讨论了如何使用 messageSW 给 Service Worker 发送消息、如何接收来自 Service Worker 的消息、页面与 Service Worker 相互通信时的注意事项。
通过本章的学习,相信大家已能轻松应对 Service Worker 注册、更新中所遇到的问题,下一章,我们将讨论 Workbox 最后一个主题:构建。